<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SignalRGB 设备模型编辑器</title>
<style>
  :root { --panel-w: 320px; --canvas-size: 700px; }
  body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Microsoft Yahei", Arial; margin: 18px; background: #fafafa; color:#222;}
  h1 { margin: 0 0 10px 0; font-size:18px;}
  .wrap { display:flex; gap:18px; align-items:flex-start; }
  .left { width: var(--panel-w); background:#fff; padding:12px; border-radius:8px; box-shadow:0 2px 6px rgba(0,0,0,0.08); }
  .left label { display:block; margin:8px 0 4px; font-size:13px; }
  input[type="text"], input[type="number"] { width:100%; box-sizing:border-box; padding:6px 8px; border:1px solid #ddd; border-radius:6px; }
  button { margin:8px 6px 0 0; padding:7px 12px; border-radius:6px; border:0; background:#2f7efc; color:#fff; cursor:pointer;}
  button.secondary { background:#666; }
  button.warn { background:#e44; }
  .meta { font-size:13px; margin-top:6px; }
  .canvas-wrap { background:#fff; padding:12px; border-radius:8px; box-shadow:0 2px 6px rgba(0,0,0,0.08); }
  canvas { background: linear-gradient(#fff,#fff); display:block; border:1px solid #ccc; cursor:crosshair; }
  .controls { margin-top:10px; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
  pre { background:#0f1724; color:#e6eef8; padding:10px; border-radius:6px; max-height:220px; overflow:auto; font-size:12px; }
  .stat { font-weight:600; margin-top:8px; }
  .hint { font-size:12px; color:#666; margin-top:6px; }
  .fileinput { display:none; }
  footer { margin-top:12px; font-size:12px; color:#666; }
  .row { display:flex; gap:8px; }
  .auto-gen { font-size:11px; color:#2f7efc; margin-top:2px; }
</style>
</head>
<body>
  <h1>SignalRGB 网格 / 风扇模型可视化编辑器</h1>
  <div class="wrap">
    <div class="left">
      <label>产品名（ProductName）</label>
      <input id="prodName" type="text" placeholder="请输入产品名">
      <div class="auto-gen">导出的JSON中会自动添加灯珠数量，如: 产品名 - 12LED</div>
      
      <label>品牌（Brand）</label>
      <input id="brand" type="text" placeholder="留空将自动设为CompGen">
      <div class="auto-gen">留空时品牌将自动设为: CompGen</div>
      
      <div style="display:flex; gap:8px; margin-top:8px;">
        <div style="flex:1;">
          <label>宽度（Width，格数）</label>
          <input id="gridW" type="number" min="1" value="15">
        </div>
        <div style="flex:1;">
          <label>高度（Height，格数）</label>
          <input id="gridH" type="number" min="1" value="15">
        </div>
      </div>

      <div class="row" style="margin-top:8px;">
        <div style="flex:1;">
          <label>格子像素大小（px，可调整）</label>
          <input id="cellPx" type="number" min="6" value="32">
        </div>
      </div>

      <div class="controls">
        <button id="btnGen">生成网格</button>
        <button id="btnReset" class="secondary">重置灯珠</button>
        <button id="btnClear" class="warn">清空所有</button>
      </div>

      <div style="margin-top:10px;">
        <label>导入 / 导出</label>
        <div class="controls">
          <input id="fileIn" class="fileinput" type="file" accept=".json">
          <button id="btnImport">导入</button>
          <button id="btnExport">导出</button>
        </div>
        <div class="hint">导入 SignalRGB JSON 会还原 <code>LedCoordinates</code> 坐标并填回宽高/名称（若存在）。</div>
      </div>

      <div class="meta">
        <div class="stat">灯珠数量： <span id="ledCount">0</span></div>
        <div class="stat">当前模式：<span id="mode">点选放置 / 拖动移动</span></div>
      </div>

      <div style="margin-top:10px;">
        <label>JSON 预览</label>
        <pre id="jsonOut">{}</pre>
      </div>

      <footer>
        使用方法：生成网格 → 单击格子添加 LED → 双击击删除 → 拖动已放置 LED 可移动位置。<br>
        生成后点击"导出"保存模型到本地（C:\Users\用户名\文档\WhirlwindFX\Components）。
      </footer>
    </div>

    <div class="canvas-wrap">
      <canvas id="gridCanvas" width="700" height="700"></canvas>
    </div>
  </div>

<script>
const canvas = document.getElementById('gridCanvas');
const ctx = canvas.getContext('2d');

let gridW = parseInt(document.getElementById('gridW').value,10);
let gridH = parseInt(document.getElementById('gridH').value,10);
let cellPx = parseInt(document.getElementById('cellPx').value,10);
let offset = {x:20,y:20};
let leds = []; // {x, y, animX, animY}
let dragging = null;
let hoverCell = null;

// 获取自动生成的产品名（用于导出的JSON）
function getAutoProductName() {
  const baseName = document.getElementById('prodName').value || 'CustomFan';
  const ledCount = leds.length;
  return `${baseName} - ${ledCount}LED`;
}

// 获取品牌名称（如果为空则返回CompGen）
function getBrand() {
  const brand = document.getElementById('brand').value.trim();
  return brand || 'CompGen';
}

// resize canvas
function resizeCanvasByCell() {
  const size = Math.max(gridW * cellPx, gridH * cellPx) + offset.x*2;
  canvas.width = Math.max(size, 300);
  canvas.height = Math.max(size, 300);
}

// 构建网格
function genGrid() {
  gridW = Math.max(1, parseInt(document.getElementById('gridW').value,10));
  gridH = Math.max(1, parseInt(document.getElementById('gridH').value,10));
  cellPx = Math.max(6, parseInt(document.getElementById('cellPx').value,10));
  resizeCanvasByCell();
  // 保留网格内的 LED
  leds = leds.filter(p => p.x >=0 && p.x < gridW && p.y >=0 && p.y < gridH);
  drawAll();
}

// 清空 LED
function clearLeds() { 
  leds = []; 
  updateUI(); 
  drawAll(); 
}

function resetLeds() { 
  leds = []; 
  drawAll(); 
  updateUI(); 
}

// 将像素坐标映射到格子
function pointToCell(px, py) {
  const x = Math.floor((px - offset.x) / cellPx);
  const y = Math.floor((py - offset.y) / cellPx);
  if (x < 0 || x >= gridW || y < 0 || y >= gridH) return null;
  return {x, y};
}

// 找 LED 索引
function findLedAtCell(cell) {
  if (!cell) return -1;
  for (let i=0;i<leds.length;i++){
    if (leds[i].x === cell.x && leds[i].y === cell.y) return i;
  }
  return -1;
}

// 更新 UI 和 JSON 预览
function updateUI() {
  document.getElementById('ledCount').textContent = leds.length;
  document.getElementById('jsonOut').textContent = JSON.stringify(buildJsonPreview(), null, 2);
}

// 构建 JSON（用于预览）
function buildJsonPreview() {
  const prod = document.getElementById('prodName').value || 'CustomFan';
  const brand = getBrand();
  const coords = leds.map(p => [p.x, p.y]);
  return {
    ProductName: prod,
    DisplayName: prod, // 显示名跟随产品名
    Brand: brand,
    Type: "custom",
    LedCount: coords.length,
    Width: gridW,
    Height: gridH,
    LedMapping: coords.map((_,i)=>i),
    LedCoordinates: coords,
    LedNames: coords.map((_,i)=>"Led"+(i+1))
  };
}

// 构建导出用的JSON（自动添加灯珠数量）
function buildExportJson() {
  const prod = getAutoProductName();
  const brand = getBrand();
  const coords = leds.map(p => [p.x, p.y]);
  return {
    ProductName: prod,
    DisplayName: prod, // 显示名跟随产品名
    Brand: brand,
    Type: "custom",
    LedCount: coords.length,
    Width: gridW,
    Height: gridH,
    LedMapping: coords.map((_,i)=>i),
    LedCoordinates: coords,
    LedNames: coords.map((_,i)=>"Led"+(i+1))
  };
}

// 绘制 LED
function drawAll() {
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.fillStyle = '#fff';
  ctx.fillRect(0,0,canvas.width,canvas.height);

  // 网格
  ctx.strokeStyle = '#999';
  ctx.lineWidth = 1;
  for (let i=0;i<=gridW;i++) {
    const x = offset.x + i*cellPx;
    ctx.beginPath();
    ctx.moveTo(x, offset.y);
    ctx.lineTo(x, offset.y + gridH*cellPx);
    ctx.stroke();
  }
  for (let j=0;j<=gridH;j++) {
    const y = offset.y + j*cellPx;
    ctx.beginPath();
    ctx.moveTo(offset.x, y);
    ctx.lineTo(offset.x + gridW*cellPx, y);
    ctx.stroke();
  }

  // 标签
  ctx.fillStyle = '#111';
  ctx.font = '14px sans-serif';
  ctx.textAlign = 'center';
  for (let i=0;i<gridW;i++) ctx.fillText(i, offset.x + i*cellPx + cellPx/2, offset.y - 8);
  ctx.textAlign = 'right';
  for (let j=0;j<gridH;j++) ctx.fillText(j, offset.x - 8, offset.y + j*cellPx + cellPx/2 + 6);

  // hover
  if (hoverCell) {
    ctx.fillStyle = 'rgba(47,126,252,0.12)';
    ctx.fillRect(offset.x + hoverCell.x*cellPx+1, offset.y + hoverCell.y*cellPx+1, cellPx-1, cellPx-1);
  }

  // draw leds with smooth animation
  leds.forEach((pt, idx)=>{
    if (pt.animX === undefined) pt.animX = pt.x;
    if (pt.animY === undefined) pt.animY = pt.y;
    // 平滑过渡
    pt.animX += (pt.x - pt.animX)*0.2;
    pt.animY += (pt.y - pt.animY)*0.2;

    const cx = offset.x + pt.animX*cellPx + cellPx/2;
    const cy = offset.y + pt.animY*cellPx + cellPx/2;
    ctx.beginPath();
    ctx.fillStyle = '#2f7efc';
    ctx.arc(cx, cy, Math.max(6, cellPx/3), 0, Math.PI*2);
    ctx.fill();

    ctx.fillStyle = '#fff';
    ctx.font = `${Math.max(10, cellPx/6)}px sans-serif`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(idx, cx, cy);

    if (dragging === idx) {
      ctx.strokeStyle = '#ffa500';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(cx, cy, Math.max(6, cellPx/3)+4, 0, Math.PI*2);
      ctx.stroke();
    }
  });

  requestAnimationFrame(drawAll);
}

// 鼠标事件
canvas.addEventListener('mousemove', (e)=>{
  const r = canvas.getBoundingClientRect();
  const p = {x: e.clientX - r.left, y: e.clientY - r.top};
  const cell = pointToCell(p.x, p.y);
  hoverCell = cell;

  if (dragging !== null && cell) leds[dragging] = {x: cell.x, y: cell.y};
});

canvas.addEventListener('mousedown', (e)=>{
  const r = canvas.getBoundingClientRect();
  const p = {x: e.clientX - r.left, y: e.clientY - r.top};
  const cell = pointToCell(p.x, p.y);
  if (!cell) return;
  const idx = findLedAtCell(cell);
  if (idx >= 0) dragging = idx;
  else { 
    leds.push({x: cell.x, y: cell.y}); 
    updateUI(); 
  }
});

canvas.addEventListener('mouseup', (e)=>{
  if (dragging === null) return;
  const r = canvas.getBoundingClientRect();
  const p = {x: e.clientX - r.left, y: e.clientY - r.top};
  const cell = pointToCell(p.x, p.y);
  if (cell) {
    const other = findLedAtCell(cell);
    if (other >= 0 && other !== dragging) {
      const tmp = { ...leds[other] };
      leds[other] = { ...leds[dragging] };
      leds[dragging] = tmp;
    } else leds[dragging] = {x: cell.x, y: cell.y};
  }
  dragging = null;
  updateUI();
});

canvas.addEventListener('mouseleave', ()=>{ hoverCell=null; });
canvas.addEventListener('dblclick', (e)=>{
  const r = canvas.getBoundingClientRect();
  const p = {x:e.clientX-r.left, y:e.clientY-r.top};
  const cell = pointToCell(p.x, p.y);
  const idx = findLedAtCell(cell);
  if(idx>=0){ 
    leds.splice(idx,1); 
    updateUI(); 
  }
});

// Buttons
document.getElementById('btnGen').addEventListener('click', ()=>genGrid());
document.getElementById('btnReset').addEventListener('click', ()=>resetLeds());
document.getElementById('btnClear').addEventListener('click', ()=>{
  if(confirm('清空所有灯珠？')) clearLeds();
});

document.getElementById('btnImport').addEventListener('click', ()=>document.getElementById('fileIn').click());
document.getElementById('fileIn').addEventListener('change', (ev)=>{
  const f = ev.target.files[0]; if(!f) return;
  const reader = new FileReader();
  reader.onload = (e)=>{
    try{
      const data = JSON.parse(e.target.result);
      if(data.Width) { document.getElementById('gridW').value=data.Width; gridW=data.Width; }
      if(data.Height){ document.getElementById('gridH').value=data.Height; gridH=data.Height; }
      if(data.ProductName) document.getElementById('prodName').value=data.ProductName;
      if(data.Brand) document.getElementById('brand').value=data.Brand;
      leds=[];
      if(Array.isArray(data.LedCoordinates)) data.LedCoordinates.forEach(c=>{
        if(Array.isArray(c)&&c.length>=2){ const x=parseInt(c[0],10),y=parseInt(c[1],10); if(!Number.isNaN(x)&&!Number.isNaN(y)) leds.push({x,y}); }
      });
      resizeCanvasByCell(); 
      updateUI();
    }catch(err){ alert('导入 JSON 失败：'+err); }
  };
  reader.readAsText(f); ev.target.value='';
});

document.getElementById('btnExport').addEventListener('click', ()=>{
  const json = buildExportJson(); // 使用专门为导出构建的JSON
  const blob = new Blob([JSON.stringify(json,null,2)],{type:'application/json'});
  const a = document.createElement('a'); a.href=URL.createObjectURL(blob);
  a.download = (json.ProductName||'model')+'.json'; a.click();
});

// init
resizeCanvasByCell();
drawAll();
updateUI();
</script>
</body>
</html>
